﻿/******************************************************************************************************
Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************************************/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;

namespace SharpGeo.IO.ShapeFiles
{
    /// <summary>
    /// Represents a DBase File loaded into memory
    /// </summary>
    public class DBaseGraph : IEnumerable<object[]>
    {
        #region Constructor

        /// <summary>
        /// Constructs a DBaseGraph
        /// </summary>
        public DBaseGraph()
        {
        }

        #endregion Constructor

        #region Fields

        private List<DBaseFieldDescriptorGraph> _fieldDescriptors = new List<DBaseFieldDescriptorGraph>();
        private object[] _data = null;
        private int _numberOfRows = 0;

        #endregion Fields

        #region Properties

        /// <summary>
        /// Gets or sets the date that this was last updated
        /// </summary>
        public DateTime LastUpdated { get; set; }

        /// <summary>
        /// Gets or sets the number of rows of data in this DBaseGraph (Note: this can only be set prior to accessing the data)
        /// </summary>
        public int NumberOfRows
        {
            get { return _numberOfRows; }
            set
            {
                //only allow the number of rows to be set if there is no data
                if (_data == null)
                    _numberOfRows = value;
            }
        }

        /// <summary>
        /// Gets or sets the language information for this DBaseGraph
        /// </summary>
        public int LanguageId { get; set; }

        /// <summary>
        /// Gets the FieldDescriptors as a ReadOnlyCollection
        /// </summary>
        public ReadOnlyCollection<DBaseFieldDescriptorGraph> FieldDescriptors
        {
            get
            {
                return _fieldDescriptors.AsReadOnly();
            }
        }

        /// <summary>
        /// Gets and sets data at the corresponding row,column
        /// </summary>
        /// <param name="row">The data row</param>
        /// <param name="column">The data column</param>
        /// <returns>The stored data</returns>
        public object this[int row, int column]
        {
            get
            {
                if (_data == null || !(row < _numberOfRows && column < _fieldDescriptors.Count))
                {
                    return null;
                }

                return _data[(row * _fieldDescriptors.Count) + column];
            }
            set
            {
                if (!(row < _numberOfRows || column < _fieldDescriptors.Count))
                {
                    return;
                }

                if (_data == null)
                {
                    _data = new object[_numberOfRows * _fieldDescriptors.Count];
                }
                _data[(row * _fieldDescriptors.Count) + column] = value;
            }
        }

        #endregion Properties

        #region Methods

        /// <summary>
        /// Adds a field descriptor (Note: This can only be done prior to accessing the data)
        /// </summary>
        /// <param name="fieldDescriptor"></param>
        public void AddFieldDescriptor(DBaseFieldDescriptorGraph fieldDescriptor)
        {
            //only allow field descriptors to be added if there is no data in this graph
            if (_data == null)
            {
                _fieldDescriptors.Add(fieldDescriptor);
            }
        }

        /// <summary>
        /// Adds data to this DBaseGraph
        /// </summary>
        /// <param name="row">The row to add the data</param>
        /// <param name="column">The column to add the data</param>
        /// <param name="rawData">The raw data to convert and add</param>
        public void AddData(int row, int column, byte[] rawData)
        {
            if (!(row < _numberOfRows && column < _fieldDescriptors.Count))
            {
                return;
            }

            //if the data has yet been created, create it
            if (_data == null)
            {
                _data = new object[_numberOfRows * _fieldDescriptors.Count];
            }

            _data[(row * _fieldDescriptors.Count) + column] =
                ConvertFromRawData(rawData, _fieldDescriptors[column].FieldType);
        }

        /// <summary>
        /// Returns a data row
        /// </summary>
        /// <param name="row">The row to return</param>
        /// <returns>A data row</returns>
        public object[] GetRow(int row)
        {
            if (_data == null || !(row < _numberOfRows))
            {
                return null;
            }

            var r = new object[_fieldDescriptors.Count];

            for (int i = 0; i < _fieldDescriptors.Count; i++)
            {
                r[i] = _data[(row * _fieldDescriptors.Count) + i];
            }

            return r;
        }

        /// <summary>
        /// Returns an IEnumerator that enumerates through the data rows
        /// </summary>
        /// <returns>The IEnumerator</returns>
        public IEnumerator<object[]> GetEnumerator()
        {
            if (_data == null)
                yield break;

            for (int i = 0; i < _numberOfRows; i++)
            {
                yield return GetRow(i);
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #region Helper Functions

        private static object ConvertFromRawData(byte[] rawData, DBaseFieldType fieldType)
        {
            switch (fieldType)
            {
                case DBaseFieldType.Date:
                    var dateString = Encoding.ASCII.GetString(rawData).Trim();

                    if (dateString.Length < 8 || string.IsNullOrEmpty(dateString))
                    {
                        return null;
                    }

                    int y, m, d;

                    if (!int.TryParse(dateString.Substring(0, 4), out y))
                        return null;

                    if (!int.TryParse(dateString.Substring(4, 2), out m))
                        return null;

                    if (!int.TryParse(dateString.Substring(6), out d))
                        return null;

                    return new DateTime(y + 1990, m, d);

                case DBaseFieldType.Float:
                case DBaseFieldType.Numeric:
                    var numericString = Encoding.ASCII.GetString(rawData).Trim();
                    if (string.IsNullOrEmpty(numericString))
                    {
                        return null;
                    }

                    double n;

                    if (!double.TryParse(numericString, out n))
                    {
                        return null;
                    }

                    return n;

                case DBaseFieldType.Logical:
                    var logicalString = Encoding.ASCII.GetString(rawData).Trim().ToUpperInvariant();

                    if (string.IsNullOrEmpty(logicalString))
                    {
                        return null;
                    }

                    if (logicalString == "Y" || logicalString == "T")
                    {
                        return true;
                    }
                    else if (logicalString == "N" || logicalString == "F")
                    {
                        return false;
                    }
                    else
                    {
                        return null;
                    }

                case DBaseFieldType.Timestamp:
                    var dateInt = ByteConverter.ToLittleEndianInt32(rawData, 0);
                    var timeInt = ByteConverter.ToLittleEndianInt32(rawData, 4);
                    return JulianToDateTime(dateInt).AddTicks(timeInt);

                case DBaseFieldType.AutoIncrement:
                case DBaseFieldType.Long:
                    return ByteConverter.ToLittleEndianInt32(rawData, 0);

                case DBaseFieldType.Double:
                    return ByteConverter.ToLittleEndianDouble(rawData, 0);

                case DBaseFieldType.Character:
                    return Encoding.ASCII.GetString(rawData).Trim();

                case DBaseFieldType.OleData:
                case DBaseFieldType.Binary:
                case DBaseFieldType.Memo:
                case DBaseFieldType.Unknown:
                default:
                    return null;
            }
        }

        //From https://raw.github.com/jogibear9988/DotNetSiemensPLCToolBoxLibrary/master/LibNoDaveConnectionLibrary/DBF/Util.cs
        private static DateTime JulianToDateTime(long lJDN)
        {
            double p = Convert.ToDouble(lJDN);
            double s1 = p + 68569;
            double n = Math.Floor(4 * s1 / 146097);
            double s2 = s1 - Math.Floor((146097 * n + 3) / 4);
            double i = Math.Floor(4000 * (s2 + 1) / 1461001);
            double s3 = s2 - Math.Floor(1461 * i / 4) + 31;
            double q = Math.Floor(80 * s3 / 2447);
            double d = s3 - Math.Floor(2447 * q / 80);
            double s4 = Math.Floor(q / 11);
            double m = q + 2 - 12 * s4;
            double j = 100 * (n - 49) + i + s4;
            return new DateTime(Convert.ToInt32(j), Convert.ToInt32(m), Convert.ToInt32(d));
        }

        #endregion Helper Functions

        #endregion Methods

        #region Static Methods

        /// <summary>
        /// Constructs a GeometryAttributesDictionary from a DBase row
        /// </summary>
        /// <param name="graph">The DBase graph</param>
        /// <param name="row">The row to use</param>
        /// <returns>A constructed GeometryAttributesDictionary</returns>
        public static GeometryAttributes ToGeometryAttributes(DBaseGraph graph, int row)
        {
            if (graph == null || !(row < graph.NumberOfRows))
            {
                return null;
            }

            var geomAttrib = new GeometryAttributes();

            for (int i = 0; i < graph.FieldDescriptors.Count; i++)
            {
                var value = graph[row, i];

                var stringValue = string.Empty;

                if (value != null)
                {
                    stringValue = value.ToString();
                }

                geomAttrib.AddAttribute(graph.FieldDescriptors[i].FieldName, stringValue);
            }

            return geomAttrib;
        }

        #endregion Static Methods
    }
}